package auth

import (
	"errors"
	"fmt"
	"github.com/casbin/casbin/v2"
	casbinModel "github.com/casbin/casbin/v2/model"
	"github.com/casbin/casbin/v2/persist"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"smart-sensor-cloud/internal/config"
	"smart-sensor-cloud/internal/model"
	"time"
)

type (
	CasbinAdapter struct{}

	CasbinPolicySyncRequest struct {
		enforcer *casbin.SyncedEnforcer
	}
)

const (
	SyncedEnforcerKey = "SYNCED_ENFORCER"
)

var casbinPolicySyncedLoadC chan *CasbinPolicySyncRequest

func init() {
	casbinPolicySyncedLoadC = make(chan *CasbinPolicySyncRequest, 1)
	go func() {
		for {
			select {
			case req := <-casbinPolicySyncedLoadC:
				err := req.enforcer.LoadPolicy()
				if err != nil {
					logrus.Errorf("Sync load casbin policy failed, %s", err.Error())
				}
			}
		}
	}()
}

func InitCasbin(conf *config.CasbinConfig) (*casbin.SyncedEnforcer, error) {
	if !conf.Enabled {
		return nil, nil
	}

	if conf.AuthModel == "" {
		return nil, errors.New("must specified one auth model config")
	}

	enforcer, err := casbin.NewSyncedEnforcer(conf.AuthModel)
	if err != nil {
		return nil, err
	}
	enforcer.EnableLog(conf.LogEnabled)

	if err := enforcer.InitWithModelAndAdapter(enforcer.GetModel(), &CasbinAdapter{}); err != nil {
		return nil, err
	}
	enforcer.EnableEnforce(conf.Enabled)

	if conf.AutoLoad {
		enforcer.StartAutoLoadPolicy(time.Duration(conf.AutoLoadInterval) * time.Second)

	}

	return enforcer, nil
}

func (c *CasbinAdapter) LoadPolicy(model casbinModel.Model) error {
	if err := c.loadRoleAsPolicy(model); err != nil {
		logrus.Errorf("Load casbin role policy error: %s", err)
		return err
	}

	if err := c.loadUserAsPolicy(model); err != nil {
		logrus.Errorf("Load casbin user policy error: %s", err)
		return err
	}
	return nil
}

// loadRoleAsPolicy into Casbin adapter with pattern: (p, role_id, path, method)
func (c *CasbinAdapter) loadRoleAsPolicy(m casbinModel.Model) error {
	roles, err := model.GetAllRoles()
	if err != nil {
		return err
	}
	roleMenuRel, err := model.GetAllRoleMenuRelations()
	if err != nil {
		return err
	}
	roleMenuRelMap := roleMenuRel.ToMapRoleIDAsKey()

	menuActionResources, err := model.GetAllActionResources()
	if err != nil {
		return err
	}
	menuActionResourcesMap := menuActionResources.ToMapActionIDAsKey()

	for _, role := range roles {
		uniqueM := make(map[string]struct{})
		if roleMenus, ok := roleMenuRelMap[role.ID]; ok {
			for _, actionID := range roleMenus.ToActionIDs() {
				if mars, ok := menuActionResourcesMap[actionID]; ok {
					for _, mar := range mars {
						if mar.Path == "" || mar.Method == "" {
							continue
						} else if _, ok := uniqueM[mar.Path+mar.Method]; ok {
							continue
						}
						uniqueM[mar.Path+mar.Method] = struct{}{}
						policyLine := fmt.Sprintf("p,%d,%s,%s", role.ID, mar.Path, mar.Method)
						persist.LoadPolicyLine(policyLine, m)
					}
				}
			}
		}
	}
	return nil
}

func (c *CasbinAdapter) loadUserAsPolicy(m casbinModel.Model) error {
	users, err := model.GetAllUsers()
	if err != nil {
		return err
	} else if len(users) > 0 {
		userRoleRel, err := model.GetAllUserRoleRelations()
		if err != nil {
			return err
		}

		userRoleRelMap := userRoleRel.ToMapUserIDAsKey()

		for _, user := range users {
			if urs, ok := userRoleRelMap[user.ID]; ok {
				for _, ur := range urs {
					policyLine := fmt.Sprintf("g,%d,%d", ur.UserID, ur.RoleID)
					persist.LoadPolicyLine(policyLine, m)
				}
			}
		}
	}
	return nil
}

func (c *CasbinAdapter) SavePolicy(model casbinModel.Model) error {
	return nil
}

func (c *CasbinAdapter) AddPolicy(sec string, ptype string, rule []string) error {
	return nil
}

func (c *CasbinAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
	return nil
}

func (c *CasbinAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
	return nil
}

func LoadCasbinPolicy(ctx *gin.Context) {
	enforcer, ok := ctx.Get(SyncedEnforcerKey)
	if !ok {
		logrus.Warn("Get casbin enforcer failed, maybe the casbin not enabled.")
		return
	}

	if len(casbinPolicySyncedLoadC) > 0 {
		logrus.Info("Sync load casbin policy is already in the queue")
		return
	}

	casbinPolicySyncedLoadC <- &CasbinPolicySyncRequest{enforcer: enforcer.(*casbin.SyncedEnforcer)}
}
